package serverDB_classes;


import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Set;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import sun.reflect.ReflectionFactory.GetReflectionFactoryAction;

import dropbox.common.Command;
import dropbox.common.CreateFileCommand;
import dropbox.common.CreateFolderCommand;
import dropbox.common.DeleteCommand;
import dropbox.common.RenameCommand;

import files_handler.FilesManager;
import files_handler.UpdateInfo;
/**
 * This class offers API for integration with the 
 * Server's Data Base, this class is observable
 * other classes can register to get notified about
 * a change that was made in the data base
 * 
 * session to DB will be open once class is called
 * 
 * --Singleton--
 * 
 * @author Yoav
 *
 */
public class DBManager extends Observable{
	//holds an instance of dir manager.
	private DirManager dirManager;
	//holds an instance of user files manager.
	private cUserFilesManager userFilesManager;
	//holds an instance of user data manager.
	private UserManager userManager;
	//holds an instance of checkSum manager.
	private CheckSumsManager checkSumManager;
	//holds the session
	private Session session;
	//holds an instance of dir manager.
	private SessionFactory sessionFactory;
	//--Singleton implementation--//	
	private static class DBManagerHolder {
		static final DBManager dbManagerHolder=new DBManager();
	}
	
	//constructor
	private DBManager() {		
		dirManager=new DirManager();
		userFilesManager=new cUserFilesManager();
		userManager=new UserManager();
		checkSumManager=new CheckSumsManager();
		openSession();
	}
	/**
	 * get DBManager instance
	 * @return DBManager instance
	 */
	public static DBManager getDBManager(){
		return DBManagerHolder.dbManagerHolder;
	}	
	/*
	 *open a new session 
	 */
	private void openSession(){
		sessionFactory=new Configuration().configure().buildSessionFactory(); 
		session=sessionFactory.openSession();		
	}
	/*
	 * close a session 
	 * @param session - session to close
	 * @param sessionFactory - session's factory
	 */
	private void closeSession(Session session,SessionFactory sessionFactory){
		session.close();
		sessionFactory.close();
		session=null;
		sessionFactory=null;
	}	
	
	/**
	 * create a new user in data base
	 * @param userName - the user's name
	 * @param password - the user's password
	 * @param firstName - the user's firstName
	 * @param lastName - the user's lastName
	 */
	public void newUser(String userName,String password,String firstName,String lastName){			
		userManager.createUser(session,userName, password, firstName, lastName);
	}
	
	/**
	 * rename a directory in data base
	 * @param userName - user's name that renamed a dir
	 * @param oldPath - dir's old path
	 * @param newPath - dir's new path
	 */
	public void renameDir(String userName,String oldPath,String newPath){
		//get relevant dir
		String getDir="Select dir From Dir dir join dir.users user WHERE dir.dirPath =:dirpath and user.userName =:self ";
		Query query=session.createQuery(getDir)
		.setParameter("dirpath",oldPath)
		.setParameter("self", userName);
		
		Dir changedDir=(Dir)(query.list().iterator().next());		
		if (changedDir!=null){
			//change dir's path
			changedDir.setDirPath(newPath);
			dirManager.update(session, changedDir);
			//create a new command to rename the dir (see javaDoc for Command)
			Command change= new RenameCommand(oldPath, newPath);
			//notify users about the change
			notifyUsers(newPath,userName,change);
		}		
	}
	/**
	 * rename a file in data base
	 * @param userName - user's name that renamed a file
	 * @param oldPath - old path of file
	 * @param newPath - new path of file
	 */
	public void renameFile(String userName,String oldPath,String newPath){
		//get relevant file
		String dirPath=oldPath.substring(0,oldPath.lastIndexOf(System.getProperty("file.separator")));
		String getFile="Select file From Dir dir join dir.files file join dir.users user " +
						"WHERE file.filePath =:filepath" +
						" and dir.dirPath=:dirpath and user.userName =:self ";
		Query query=session.createQuery(getFile)		
			.setParameter("dirpath", dirPath)
			.setParameter("filepath",oldPath)
			.setParameter("self", userName);
		
		UserFiles changedFile=(UserFiles)query.list().iterator().next();		
		if (changedFile!=null){
			//change file's name
			changedFile.setFilePath(newPath);
			userFilesManager.update(session, changedFile);
			Command change= new RenameCommand(oldPath, newPath);
			//notify users about the change
			notifyUsers(dirPath,userName,change);
		}
		
	}
	/**
	 * create a new dir in data base
	 * @param userName - user's name that wants to add new dir
	 * @param dirPath - new dir's path
	 */
	public void newDir(String userName,String dirPath){
		//create a new dir
		Dir dir=dirManager.createDir(session,dirPath);
		//create a new command that dir was created
		Command change=new CreateFolderCommand(dirPath);
		//get root dir if new dir is a sub dir
		String root=dirPath.substring(0,dirPath.indexOf(System.getProperty("file.separator")));
		Dir rootDir=getDir(userName, root);
		
		//link new dir to all relevant users in data base
		for (UserData username : rootDir.getUsers()){
			username.getDirs().add(dir);
			userManager.updateUser(session, username);
		}		
		//notify users about the change
		notifyUsers(root,userName,change);
	}
	/*
	 * get dir or file name given its path 
	 * @param path - dir/file 's path 
	 * @return dir/file 's name
	 */
	private String getNameByPath(String path){
		//trim all chars until last file separator
		String name=path.substring(path.lastIndexOf(System.getProperty("file.separator"))+1
				, path.length());
		
		return name;
	}
	/**
	 * get a list of files in a given folder path
	 * @param userName - user's name to which files belong
	 * @param dirPath - given dir path to search for file in
	 * @return list of files in given dir path or null if no files
	 */
	public List<String> getRelativeFiles(String userName,String dirPath){		
		List<String> items=null;		
		if (dirPath!=null){			
			//get relevant files from data base
			String getDir="Select file From Dir dir join dir.files file join dir.users user " +
						"WHERE " +
						" dir.dirPath=:dirpath and user.userName =:self ";
			Query query=session.createQuery(getDir)
			.setParameter("dirpath",dirPath)
			.setParameter("self", userName);
			
			List<UserFiles> files=(List<UserFiles>)query.list();
			
			items=new ArrayList<String>();
			//insert all found file names to returned list
			for (UserFiles file : files){										
				items.add(getNameByPath(file.getFilePath()));
			}
		}
		return items;	
	}
	
	/**
	 * get a list of folders in a given dir path
	 * @param userName - user's name to which dir belong
	 * @param dirPath - given dir path to search for file in
	 * @return list of dirs in given dir path or null if no dirs
	 */
	public List<String> getRelativeDirs(String userName,String dirPath){
		List<String> items=null;		
		if (dirPath!=null){	
			//find user in data base
			UserData user=getUser(userName);			
			if (user!=null){
				items=new ArrayList<String>();
				//go over his dirs and add them to returned list
				for (Dir dir : user.getDirs()){
					if (dir.getDirPath().startsWith(dirPath) && !dir.getDirPath().equals(dirPath)){
						items.add(getNameByPath(dir.getDirPath()));			
					}
				}
			}
		}
		return items;	
	}
	/**
	 * check if given path is a dir or a file
	 * @param path - given path
	 * @param userName - user's name
	 * @return true if path is a dir , false OW
	 */
	public boolean isDir(String path,String userName){
		boolean answer=false;
		//try to get dir from DB that fits to given path and user name
		String getDir="Select user.userName From UserData user join user.dirs dir WHERE dir.dirPath =:dirpath and user.userName =:self ";
		Query query=session.createQuery(getDir)
		.setParameter("dirpath",path)
		.setParameter("self", userName);
		//if found
		if (query.list().size()==1){
			//true
			answer=true;
		}
		//false
		return answer;
	}
	/**
	 * create a new file in data base
	 * @param checkSum - file's check sum
	 * @param filePath - file's path
	 * @param userName - user name that added the file
	 */
	public void newFile(String checkSum,String filePath,String userName){
		//get file's checkSum instance from data base
		CheckSums check=findCheckSum(checkSum);
		if (check!=null){			
			//get user instance from data base
			UserData user=getUser(userName);			
			if (user!=null){
				//extract dir name of given file
				String dirPath=filePath.substring(0,filePath.lastIndexOf(System.getProperty("file.separator")));
				//check if file already exists
				UserFiles existFile=findFile(user.getDirs(),filePath);
					
					if (existFile==null){

						//new file uploaded						
						UserFiles file=userFilesManager.createFile(session,check,filePath);					

						//update dir about new file
						Dir dir=getDir(userName,dirPath);					
						if (dir!=null){
							dir.getFiles().add(file);
							dirManager.update(session,dir);
						}					
						
					}
					else{

						//update old file						
						String oldCheckSum=existFile.getCheckSum().getCheckSum();
						existFile.setCheckSum(check);	
						userFilesManager.update(session,existFile);
						//check if no more files points to old checkSum
						if (getChkSumRefCount(oldCheckSum)==0){
							//delete file from server
							FilesManager.getFilesManager().deleteFile(oldCheckSum);
						}
					}
					//create a new command
					Command change=new CreateFileCommand(checkSum,filePath);
					//notify user about the change
					notifyUsers(dirPath,userName,change);	
				}			
				
			}
	}

	/*
	 * notify users about a change 
	 * @param dirPath - dir that was changed
	 * @param userName - user's name that made the change
	 * @param change - the change that was made
	 * 
	 */
	private void notifyUsers(String dirPath,String userName,Command change){
		//get all users who listen to dir path folder
		String getAffectedUsers="Select user.userName From UserData user join " +
					"user.dirs dir WHERE dir.dirPath =:dirpath and user.userName !=:self ";
		Query query=session.createQuery(getAffectedUsers)
		.setParameter("dirpath",dirPath)
		.setParameter("self", userName);
		List<String> users=(List<String>)query.list();
		if (users.size()>0){
			//add found users to update info class (see java doc) and add the change 
			this.setChanged();
			UpdateInfo updateInfo=new UpdateInfo(users,change);
			//notify listeners
			this.notifyObservers(updateInfo);
		}		
	}
	
	/*
	 * get a dir by given user name and a dir path 
	 * @param userName - given user name
	 * @param dirPath - a dir's path
	 * @return dir instance that match to given input,
	 * 			null if no dir was found
	 */
	private Dir getDir(String userName, String dirPath) {
		//get user instance
		String findDir="From UserData Where userName='"+userName+"'";
		Query query=session.createQuery(findDir);		
		List <UserData> users=(List<UserData>)query.list();
		Dir foundDir=null;
		//search in user's dir for required dir
		for (Dir dir: users.get(0).getDirs()){
			if (dir.getDirPath().equals(dirPath)){
				foundDir=dir;
				break;
			}
		}		
		return foundDir;
	}
	/**
	 * verify if user name and password exist in data base
	 * @param userName - user's name
	 * @param password - user's password
	 * @return - true if exists , false OW
	 */
	public boolean verifyUser(String userName,String password) {
		boolean answer=false;
		
		if (userName!=null && password!=null){
			//get proper user name from data base according to given input
			String hql = "From UserData user Where user.userName=:username and user.password=:password";
			Query query=session.createQuery(hql)
						.setParameter("username",userName)
						.setParameter("password",password);
			//check if found the user in data base
			answer= (query.list().size() == 1);
		}
		return answer;
	}
	/**
	 * insert a new checkSum to data base
	 * @param checkSum value of new checkSum
	 */
	public void newCheckSum(String checkSum) {
		//create ceckSum and add it to data base
		checkSumManager.create(session,checkSum);
	}
	
	/**
	 * get UserData instance from data base
	 * @param userName - given user's name
	 * @return UserData instance if exists, null OW 
	 */
	private UserData getUser(String userName){

		//find user in data bse
		String getUserName="From UserData user Where user.userName=:userName";
		Query query=session.createQuery(getUserName).setParameter("userName",userName);
		UserData userFound=null;
		//check if user was found
		if (query.list().size()==1){
			userFound=(UserData) query.list().get(0);
		}
		return userFound;
	}
	/**
	 * delete a file from data base
	 * @param filePath - the file's path
	 * @param userName - user's name who deleted the file
	 * @return deleted file's checkSum
	 */
	public String deleteFile(String filePath,String userName) {
		//get user name instance
		UserData user=getUser(userName);
		String checkSum=null;
		//find the deleted file
		UserFiles file=findFile(user.getDirs(),filePath);		
		if (file!=null){
			//get file's checkSum
			checkSum=file.getCheckSum().getCheckSum();
			//get file's dir
			String dirPath=filePath.substring(0,filePath.indexOf(System.getProperty("file.separator")));
			//update data base
			userFilesManager.delete(session,file);
			//create a new delete command
			Command change=new DeleteCommand(filePath);
			//notify users about the change
			notifyUsers(dirPath, userName, change);
		}	
		return checkSum;	
	}
	/**
	 * delete a dir from data base
	 * @param dirPath - given dir path
	 * @param userName - user's name who deleted the dir
	 */
	public void deleteDir(String dirPath,String userName){
		//get user from data base
		UserData user=getUser(userName);
		//go over user's folders
		for (Dir dir : user.getDirs()){
			
			/*check if given folder was found or 
			folder is a sub folder of it*/			
			if (dir.getDirPath().startsWith(dirPath)){
				//true, empty from files
				emptyDir(dir);
				//delete dir from data base 
				dirManager.delete(session, dir);
			}
		}
		//create a new delete command
		Command change=new DeleteCommand(dirPath);
		//notify users about the change
		notifyUsers(dirPath, userName, change);
		
	}
	/*
	 * empty a given dir 
	 * @param dir - given Dir instance
	 */
	private void emptyDir(Dir dir){
		//go over all files in folder
		for (UserFiles file : dir.getFiles()){
			//delete files from data base
			String fileCheckSum=file.getCheckSum().getCheckSum();
			userFilesManager.delete(session, file);
			//check if deleted file's checkSum has no more references
			if (getChkSumRefCount(fileCheckSum)==0){
				//true, delete from data base
				files_handler.FilesManager.getFilesManager().deleteFile(fileCheckSum);
			}
		}		
	}
	/*
	 * find a file in given dirs 
	 * @param dirs - given dirs 
	 * @param filePath - the file's path
	 * @return - the file if found, null OW
	 */
	private UserFiles findFile(Set<Dir> dirs,String filePath){
		//go over all dirs
		for (Dir dir : dirs){
			//go over all files in dir
			for (UserFiles file : dir.getFiles()){
				//compare file's name
				if (file.getFilePath().equals(filePath)){
					return file;
				}
			}
		}
		return null;
	}
	/**
	 * check if file exists in data base
	 * @param checkSum file's checkSum
	 * @return true if file exists, false OW
	 */
	public boolean isFileExists(String checkSum) {
		
		boolean answer=false;
		//get file from data base
		String getCheckSums="From CheckSums checkSum Where checkSum.checkSum=:checkSum";
		Query query=session.createQuery(getCheckSums).setParameter("checkSum",checkSum);
		//check if file found
		if (query.list().size()>0){
			answer=true;
		}
		return answer;
	}
	/**
	 * check how many references for a given checkSum in data base
	 * @param checkSum given checkSum
	 * @return number of file paths in data base 
	 * 			that points to given checkSum
	 */
	public long getChkSumRefCount(String checkSum){
		//get ref count from data base
		String getRefCount="Select count(file) From UserFiles file Where file.checkSum.checkSum=:checkSum Group By file.checkSum";
		long refCount=0;
		try{
			refCount=(Long)session.createQuery(getRefCount).setString("checkSum", checkSum).iterate().next();
		}
		catch (Exception e){
			//no ref found to given checkSum
			refCount=0;
		}
		return refCount;
	}
	
	/**
	 * get a checkSum instance by a checkSum value
	 * @param checkSum - checkSum value
	 * @return checkSum instance if checkSum is in data base
	 */
	public CheckSums findCheckSum(String checkSum){
		CheckSums chk=null;
		//get checkSum from data base
		String hql = "From CheckSums checksum Where checksum.checkSum=:check";
		Query query = session.createQuery(hql).setString("check", checkSum);
		//check if found
		if (query.list().size()>0){
			chk= (CheckSums)query.list().get(0);
		}
		return chk;		
	}
	/**
	 * delete checkSum from data base
	 * @param checkSum given checkSum value
	 */
	public void deleteCheckSum(String checkSum) {
		//get checkSum instance from data base
		String hql = "From CheckSums checksum Where checksum.checkSum=:check";
		Query query = session.createQuery(hql).setString("check", checkSum);
		CheckSums fileCheckSum=(CheckSums)query.list().get(0);
		//delete checkSum from data base
		checkSumManager.delete(session,fileCheckSum);
		
	}
}
